home *** CD-ROM | disk | FTP | other *** search
/ Language/OS - Multiplatform Resource Library / LANGUAGE OS.iso / scheme / scm / wb1a1.lha / wb / Design.doc < prev    next >
Encoding:
Text File  |  1993-06-25  |  32.9 KB  |  775 lines

  1. "Design.doc" for WB-tree File Based Associative String Data Base.
  2. Copyright (C) 1991, 1992, 1993 Holland Mark Martin
  3.  
  4. This is an attempt to make explicit the major decisions
  5. we've either made explicitly or arrived at though experience
  6. in the course of working on the wb-tree database.
  7.  
  8. Since there are actually a lot of them,
  9. I've divided this into the following areas:
  10.  
  11. I. B-Tree Structure and Access
  12. II. Concurrency
  13. III. Buffer, I/O, and Free-List Management
  14. IV. Error Handling
  15. V. Misc
  16.  
  17. last mod:
  18. oct 28/1992 rjz reorganized and expanded
  19. nov 19/1992 rjz recorded recent ananlysis of
  20.                 UPDATE screw and deferred updates
  21. dec  2-3/1992 rjz random elaboratins and edits
  22. dec  8-9/1992 rjz RANGE doc and extensions
  23. dec  16       rjz misc editing
  24.  
  25. jan   8/1993  rjz doc new deferred-tree-update algorithms
  26. jan  13-15    rjz more of same; add descr of concurrency
  27.                   problems w/free list, error codes, misc,
  28.           deferred data update proposal.
  29.  
  30. ---------------------------------------------------------------
  31.  
  32. I. B-Tree Structure and Access
  33.  
  34. We used SAGIV 86 as our starting point for concurrent b-tree
  35. algorithms. However, we made significant changes and (we feel)
  36. improvements. We have particularly tried to simplify the algorithms to
  37. reduce the implementation and especiaslly debugging effort. There are
  38. also a lot of complex details involved in building a complete
  39. implementation. The goal of this document is to describe and explain
  40. the major design decisions and any subtleties that arose.
  41.  
  42. 1. Tree format
  43.  
  44. 1.1 The WB-tree (the "wanna-be a tree")
  45.  
  46. We use the B-link tree format (see SAGIV 86).  Each leaf block
  47. contains some number of (key,value) pairs; each non-leaf (ie, index)
  48. block contains (key, down-pointer) pairs.  Each block contains one
  49. additional key value, the SPLIT key, that is strictly greater than any
  50. other key in the block.  The tree is augmented by chaining together
  51. the nodes of each level of the tree in split-key order (the NEXT pointers).
  52.  
  53. We allow the tree to be missing any index-insertion or  aock-deletion
  54. updates so long as certain key-order invariants are maintained:
  55.  
  56. A. Blocks are chained in order of increasing split key at each level,
  57.    and all valid blocks appear in the chain.
  58.  
  59. B. Split keys are unique at each level.
  60.  
  61. C. Every DOWN pointer from level N+1 points to a block B at the next lower
  62. level N whose split-key is less than or equal to the key associated with
  63. the pointer.
  64.  
  65. C1. If it is less, then it must be the case that (a) a block B' with
  66. that split key exists at level N; (b) it is reachable from B by
  67. following NEXT pointers; and (c) no pointers to either B' nor any
  68. blocks between B and B' exist in level N+1. We call the subchain from
  69. B through B' the SPAN of the (key,ptr) entry (split(B'),B).
  70.  
  71. C2. It is invalid for a down pointer to contain a key not present as a
  72. split key at the next level.
  73.  
  74. Note that a key in an index must match its block's split key exactly;
  75. if a key K is less than the split-key S of the block B
  76. it points to, searches for intervening keys will be misdirected (to
  77. the next block); and if K is greater than S, then splits of the block
  78. after B at key values K' where S<K'<K will be mis-inserted in B's
  79. paren, because K' should logically go AFTER K, but K'<K.
  80.  
  81. The notion of the span of an index entry is useful. We note that each
  82. block split can be thought of as an EXTENSION of some span at the
  83. next-higher level, while each parent-insert-update can be thought of
  84. as a corresponding span REDUCTION. A span that has only one block in
  85. it will be called FULLY REDUCED; a b-tree is fully reduced when all
  86. its spans are fully reduced, meaning that all pending/deferreed
  87. insert-updates have been performed. Lastly, we can express rule C1
  88. more succinctly in terms of spans:
  89.  
  90. C,C2. SPANs must be well-formed (span must be closed; keys must
  91.       match exactly)
  92. C1. The SPANS of the entries at any index must not overlap.
  93.  
  94.  
  95. 1.2 Key/value order; uniform leaf/node format
  96.  
  97. We originally had index nodes in (value,key) order because it made
  98. insertions simple, but abandoned that because it made all the
  99. insertion/deletion propagation code special-case. In fact, because of
  100. the asymmetry of INSERT and DELETE, either one or the other will
  101. require that a single operation sometimes modify two blocks (see
  102. INSERTION METHOD). However, using (key,value) ordering at all levels
  103. of the tree simplified the code considerably.
  104.  
  105. 2. Split keys
  106.  
  107. Split keys seem to be an accepted method for B-tree organization.
  108. In a Blink-tree, wherer one might potentailly have to chain forward
  109. to find a desired key, the split key allows one to determine when
  110. the search can be terminated. Having the split key at the END of
  111. the block saves one block access per search. (If the split key were at
  112. the front of the block, one would have to access the next block
  113. in the chain to determine that a search could terminate.)
  114.  
  115. 2.1 Last-key values
  116.  
  117. It is useful to define a "largest key" to be the split key of the last
  118. at any given level; the code is made more complex if the last key is
  119. some special value (eg, the null string) that doesnt follow
  120. lexicographic order, not to mention the problems that arise with
  121. inserting into empty (last) blocks.  We've reserved the set of strings
  122. starting with #\255 as split keys only; they cannot be used as real
  123. key values.  Last-key values are of the form xFF<level>. This makes
  124. the last key in level N+1 strictly larger than any in level N, so the
  125. end-of-chain key for level N need not be treated specially when it is
  126. inserted as a key at level N+1.
  127.  
  128. 2.2 Fixed split keys/Independence of levels
  129.  
  130. We make the split keys at index levels act like those at the leaf
  131. level, that is, the split key in a block NEVER CHANGES.  The advantage
  132. is that inserts and deletes cannot cause split-key changes, avoiding
  133. the need for code to propagate these changes, which are a pain to
  134. test.  It also makes the levels genuinely independent, a useful
  135. conceptual simplification.
  136.  
  137. The disadvantage is that this introduces a "dead zone" between the
  138. last key-value pair of an index block and its split key. This
  139. complicates the code slightly, and makes the average search path
  140. slightly longer than logN (by a constant factor of 1+(1/BF), where BF
  141. is the branching factor of the tree).  More annoyingly, it interacts
  142. with insertions for block splits (see INSERTION).
  143.  
  144. 3. Insertion method
  145.  
  146. Insertion in a index needs to appear atomic so that the key-order
  147. invariant is maintained. Since we are using key/value-ordered index
  148. blocks, parent-update insertion has a special extra step: we insert
  149. the new key and pointer in index block B, then swap value fields with
  150. the next entry in sequence.  Unfortunately, when the insertion is the
  151. end of the block, the next entry is in the next block B', so two
  152. blocks must be modified.  (The advantage of value/key ordering is that
  153. insertion never modifies more than one block; however, you get screwed
  154. in the code for deletions instead.) The code checks for this case and
  155. locks both blocks before proceeding. (An alternate solution considered
  156. is to back up the split key of B so that the new key would go at the
  157. front of B', but that introduces other problems.)
  158.  
  159. While the above method preserves correctness (even under concurrency),
  160. there is unfortunately a 2nd-order screw WHICH STILL NEEDS TO BE
  161. IMPLEMENTED: how to write the 2 blocks in an order that preserves
  162. correctness in case of disk crash. If only B is written, the database
  163. will contain 2 pointers to the block whose split caused the insertion;
  164. if only B' is written, the pointer correctness will be violated.  (It
  165. would seem that this kind of problem is inherent to any operation that
  166. needs to maintain consistent changes across multiple blocks.)  The fix
  167. seems to be to surround the write with a notation in the root that
  168. says this special case is being exercised and indicates what blocks
  169. were being modified (B,B') and what the pointers should be:
  170.  
  171.    Write (B,B',down-ptrs) to Blk 0
  172.    Write B'
  173.    (Write new block if B was split by insert)
  174.    Write B
  175.    Remove flags from Blk 0 and write.
  176.  
  177. We elect to write out block B' first for consistency with the case
  178. where B' is a new block created by a block split. The screw case will
  179. create a damaged DB, but the danger of deleting a doubly-pointed-at
  180. block is avoided. This is adequate information to recover directly.
  181. (JONATHAN?)
  182.  
  183. (Other alternatives were considered. Backing up the split key at next
  184. level allows a subsequent insertion to be atomic, but the backing-up
  185. operation has the same two-block consistency problem in keeping ITS
  186. parent index consistent. If the parent key isnt backed up, subsequent
  187. splits of the following block will result in incorrect updates at the
  188. next level.)
  189.  
  190. 4. Deletion
  191.  
  192. Deleting can be divided into two parts: the block to be deleted must
  193. be unlinked from both parent and predecessor, after which the block
  194. can be reclaimed.
  195.  
  196. 4.1 Unlinking the block
  197.  
  198. At the moment, we've decided to simplify the delete operation by
  199. requiring that both the parent and the previous block be available for
  200. modification, else the deletion fails (is deferred). This makes block
  201. deletion an atomic change, which avoids several problems introduced by
  202. permitting partial deletions (see CONCURRENCY).
  203.  
  204. Alternatives: WANG 90 gives a clever way to do deletion w/o PREV, by
  205. swapping blocks.  This seemed too hard to make bulletproof for
  206. concurrency so we stuck with the PREV method, which after all only
  207. does extra block accesses 1/BF of the time.
  208.  
  209. Crashes during DELETE: we still be to determine an order for the
  210. writing of the parent and predecessor blocks in a delete.
  211. We elect to write out the PARENT block first, so that if the system should
  212. crash the chain will still be intact. The block that we were trying to
  213. delete will still be in the chain, but it cannot be used, it is "dead."
  214. [Section II.3 discusses how to dead with "dead" blocks.]
  215.  
  216. 4.2 Reclaiming the block
  217.  
  218. One major change from SAGIV is that SAGIV assumed multiple copies of a
  219. block could exist, which makes reclaiming deleted blocks complex (he
  220. suggests a timeout-based protocol).  We opted instead to track how
  221. many pointers to each block are extant by adding a lock for that
  222. purpose, the NAME lock (essentially, a DELETE lock). A routine must
  223. get NAME lock on a pointer BEFORE releasing READ/WRITE on the block
  224. from which the pointer is obtained. (SAGIV's method is almost
  225. certainly more efficient in the sense that our method incurs overhead
  226. on every operation as opposed to just on the problem case of a READ
  227. request during a WRITE; several empirical studies of such tradeoffs
  228. support this conclusion.) On the other hand, NAME lock is useful for
  229. other things, such as insuring that the block you are PREVing from
  230. cant be deleted while you're looking for its parent or predecessor...
  231.  
  232. 4.3 Non-symmetry of INSERT and DELETE
  233.  
  234. It is worth remembering that INSERT and DELETE are not symmetric, in
  235. the sense that a postponed insertion is NOT equivalnt to deleting a
  236. (KEY,PTR) pair. The latter operation leaves a block whose pointer is
  237. missing unaccessible via the index, while the former leaves the block
  238. accessible through the NEXT pointer chain.
  239.  
  240. This asymmetry has been the death of more proposals for fixing various
  241. problems involving concurrency than I care to recall!
  242.  
  243. EXAMPLE:
  244.  
  245. 4.3.1. Start with blk p with split key k at level n;
  246. the index (levl n+1) contains: ...k,p,..
  247.  
  248. 4.3.2. split p at k', adding block p';
  249. the index shoudl now contain ...k',p,k,p'..
  250.  
  251. 4.3.3. Now neither deleting p nor p' yields the original index contents:
  252. If we delete p, the result is ...k,p',...
  253. If we delete p', the result is ...k',p,...
  254.  
  255. Therefore, it is not possible for a subsequent delete to "cancel"
  256. an insert; the deletes must either wait for the relevant inserts
  257. to complete or else do special work to maintain correctness.
  258.  
  259. 5. Non-delete of last block in the chain
  260.  
  261. We currently do not support deletion of the last block at any level,
  262. that is, the one with the end-of-block key.  This is because this
  263. deletion requires special case to retain the end-of-block key. One way
  264. to achieve this is to copy forward the contents of the next-to-last
  265. block, and deleting that instead.  p[There are details to be worked
  266. out, eg, preservation of correctness during this operation.]
  267.  
  268. 6. Prev
  269.  
  270. [DESCRIBE HOW IT WORKS]
  271.  
  272. SCREW CASE: FIX UNDERSTOOD BUT NOT YET IMPLEMENTED: if down-pointers
  273. are missing to blocks immediately after the first block of a level,
  274. PREV will miss those blocks.  The problem occurs when a PREV-BLOCK of
  275. block B at level N occurs, causing a PREV at level N+1. If
  276. down-pointers are missing, the block B' associated with B's split key
  277. may be some predecessor of B rather than B itself. In this case
  278. PREVing at level N+1 is wasteful but correct; it would require fewer
  279. block accesses to chain from B' than from PREV of that entry. However,
  280. if the B' entry is the FIRST at level N+1, PREV will erroneously
  281. conclude that it is at the start of the chain. It either has to (a)
  282. always look at B's entry at level N+1 to check that the current
  283. block's ptr is really up-to-date, or just check when it hits the
  284. START-OF-CHAIN.
  285.  
  286. 7. Root Block protocol
  287.  
  288. 7.1 Root uniqueness
  289.  
  290. We guarantee that the root block number never changes and thus can be
  291. used as a unique identifier for a given WB-tree. Other systems provide
  292. a unique tree ID by introducing a level of indirection in root
  293. references; this is inefficient, as root references are frequent. When
  294. the root is split, we allocate a new block to hold the data that would
  295. have remained in the root block, then use the old root block for the
  296. new root.  This does mean that one cannot depend on a block's ID being
  297. unchanged if it splits!
  298.  
  299. 7.2 NOT IMPLEMENTED: ROOT DELETE and Reducing number of levels in a
  300. tree.
  301.  
  302. 8. Other tree organizations
  303.  
  304. [There are other possibilities for tree layouts. It is possible that
  305. some of these may simplify operations that in the current layout are
  306. complex, such as the insert-screw-case. Other possible choices
  307. include:
  308.  
  309. - split key at start rather than the end of the block;
  310. - reversing the order of (key,value) pairs in the blocks;
  311. - not using the split key
  312. - using TWO split keys (one at each end of the block);
  313. - runnning the chains backearfd rather than forward;
  314.  
  315. My (RJZ's) favorite alternate assumption is allowing multiple versions
  316. of blocks to exist, as in SAGIV 86. This would mean:
  317.  
  318. - processes would be allowed to maintain STATE in a block, such as
  319.   current position, such as for successive NEXTSs; on the other
  320.   hand, NEXT would have to check for NEXT(X)<X and skip forward
  321.   accordingly.
  322. - W/R interlock overhead should be greatly reduced (as WRITEs do not
  323.   have to wait for reads);
  324. - the need for NAME locking is greatly reduced, since we no longer
  325.   would be trying to count the of processes that "know" about each
  326.   block;
  327. - reclaiming deleted blocks is harder;
  328. - other issues to be evaluated: effect on rebalanace, PREV,
  329.   and the deferred-operation rpotocol we've developed.
  330.  
  331. Perhaps we can explore the interrelationships in some
  332. methodical way someday...]
  333.  
  334.  
  335. II. Concurrency
  336.  
  337. 1. Name access (and deleted-block reclamation)
  338.  
  339. In order to be able to explicitly know when a block is safe to delete,
  340. we insist that a user must get NAME lock on a pointer BEFORE releasing
  341. READ/WRITE on the block from which the pointer is obtained.  NAME lock
  342. is useful for other things, such as
  343.  
  344. - insuring that the block you are PREVing from cant be deleted while
  345. you're looking elsewhere;
  346.  
  347. - CHAIN-PUT uses it to insure that the block just split doesnt disappear
  348. during the call to PARENT-INSERT-UPDATE
  349.  
  350. - others?
  351.  
  352.  
  353. 2. Fail-out protocol/access conflict strategy
  354.  
  355. Blocked operations are at the moment simply going to fail with an
  356. error code of RETRYERR, meaning that they can be safely retried later.
  357. THe current idea is to use this whenever a READ-WRITE conflict occurs.
  358. (This would not be necessary using SAGIV's method.)  However, since
  359. various other lockout and wait conditions can occur -- waits for block
  360. reads, waits on NAME locks, waits on interlocked operations -- some
  361. such facility would be needed anyway, so it seems reasonable to try to
  362. use it to handle READ-WRITE blocking as well.
  363.  
  364. NOT REALLY IMPLEMENTED YET due to the complexity of reorganizing the
  365. code to pass up the appropriate information in all cases.
  366. (Top-level routines return these codes but internal routines
  367. dont really use it yet, so retryability isnt really there yet.)
  368.  
  369. 3. Deferred Index Updates and concurrency
  370.  
  371. The basic strategy is to allow a key insert/delete that causes a block
  372. to split/become empty to complete, but to then queue up the
  373. parent-update/block delete on some process that will retry deferred
  374. updates in the background until they succeed.
  375.  
  376. The problem is that deletes contain two separate operations that can
  377. wait: the parent update and the predecessor update. The two updates
  378. can be done separately if the block is first marked "dead" so that no
  379. insertions into it can occur. Unfortunately, there is a class of rare
  380. and complex screw cases involving the correct ordering of deletes and
  381. inserts that happen to involve the same key.  One might for example
  382. delete a block, deleting its key, and then subsequent splits can
  383. reintroduce and re-delete it. If operations at the index level are
  384. deferred, the ordering of these deferred operations determines whether
  385. the resulting tree is correct.
  386.  
  387. Making block-delete atomic (as defined above) greatly simplifies this
  388. process. The block being inserted/deleted can then serve as its own
  389. semaphore.
  390.  
  391. We have decided to adopt a lazy update strategy. That is, rather than
  392. keeping around queues of pending parent-updates, we just throw them
  393. away if they can't be executed immmediately. We can get away with this
  394. because the updates for level N+1 can be reconstructed from the chain
  395. at level N.
  396.  
  397. Now, a deferred update only affects performance if the affected path
  398. is actually encountered: if we find we have to chain across two
  399. blocks, it means a parent update hasnt occurred yet; if we encounter
  400. an empty block, it means a delete update hasn't occurred.  Our idea is
  401. to fix these "inefficiencies" on demand, that is, only when we run
  402. into one will we expend the efort to fix it.
  403. For the moment, assume ALL block deletes and insert updates are
  404. deferred.
  405.  
  406. The basic algorithm is:
  407.  
  408. (a) If we have to chain forward in searching for a key, there must be
  409. pointer missing at the next level (deferred insert). So we attempt to
  410. insert it. Forward chaining to reach the NEXT and PREV of a key
  411. is normal and hence shouldn't cause update attempts.
  412.  
  413. (b) Whenever we reach an empty block, we attempt to delete it, except
  414. for the case where we are doing an insert which should go in that
  415. block.  The delete attempt will fail UNLESS the relevant parent updates
  416. are complete, that is, if and only if the block is within a fully
  417. reduced span.
  418.  
  419. One major concern has been that an I/O failure during a block delete
  420. can leave a "dead" block, that is, one which can't be reached.  The
  421. problem is that once a block becomes dead we need to prevent it from
  422. being inserted into or restored into use, because that could result in
  423. entries being out-of-order (suppose that, while the block's dead, some
  424. inserts of keys less than its split key occur. They'll be directed
  425. into the next block by the index, and be posted there. If we then
  426. restore the block, the key ordering will be incorrect.)  But it turns
  427. out that we can prevent this by observing which B-tree operations can
  428. encounter which types of deferred-update situation, as follows:
  429.  
  430. If we think of the tree as a set of nested SPANS, where the SPAN of an
  431. index entry is the set of entries in the blocks it SPANS, we note that
  432. during operations using search only - GET,PUT,REM -- the locus of
  433. search stays strictly within the SPAN of some entry in the root. We
  434. enforce that a block not be deleted until its parent pointers are
  435. completely updates, that is, its pointer has a span of exaclty one
  436. block.  Now suppose a block delete fails halfway, leaving a empty
  437. block without any parent pointer. Such a block is unreachable by
  438. FIND-NODE and hence by INSERT!  This means that if INSERT encounters
  439. an empty block, it must be valid to insert into it.
  440.  
  441. This has several interesting consequences:
  442.  
  443. (a) This means that only the operations that can possibly chain ACROSS
  444. spans have to worry about dead blocks: the NEXT and PREV operations.
  445. (What exactly is the effect on PREV?)
  446.  
  447. (b) This means that "dead" blocks can't be deleted (unless we look for
  448. them specially).  But they (a) are only created by a rare kind of
  449. event, and (b) won't hurt anything, so long as we arrange that NEXT
  450. and PREVs ignore the empty blocks (ie, ignore the value of their split
  451. keys).
  452.  
  453. (c) The way we have defined deferred-delete reconstruction, if the
  454. block isn't within a fully-reduced span, the delete simply can't
  455. succeed.  This means that there's no point in trying to delete a blank
  456. block when we're chaining though a span, that is, we should only try
  457. to delete empty blocks encountered (1) when following a DOWN pointer
  458. in FIND-ENT, or (2) due to a NEXT operation (or PREV?).
  459.  
  460. [Having realized this, we can actually let block-deletes be two-part
  461. operations (again). The only additional complexity is that then we'd
  462. need to implement a method for detecting dead blocks, that is,
  463. differentiating them from empty blocks in the middle of large spans,
  464. which we mustn't try to delete. We just check if the block is
  465. contined in some span! HOW TO DO THAT EFFICIENTLY?]
  466.  
  467. In detail, the method of detecting deferred operations goes like this:
  468.  
  469. 1. RECONSTRUCTING DEFERRED PARENT-UPDATES (missing DOWN pointers):
  470.  
  471. Whenever we have to follow the NEXT pointer of a block B (in FIND-NODE), we
  472. should attempt a PARENT-INSERT-UPDATE using the (key,value) pair
  473. (split(B),next(B)). FIND-NODE needs to be fixed to chain right rather
  474. than down when the "dead zone" is encountered; and it should not attempt a
  475. PARENT-INSERT-UPDATE in this case.
  476.  
  477. PARENT-INSERT-UPDATE can fail if:
  478. (a) some other process is already doing the update (the first one to
  479.     lock the parent wins, the other will fail out);
  480. (b) the key split(B) is already present (means a DELETE is pending --
  481.     this shouldn't really occur, though);
  482. (c) the update requires a block split but no blocks are available.
  483.  
  484. 2. RECONSTRUCTING DEFERRED BLOCK DELETES:
  485.  
  486. Whenever we reach an empty block in FIND-NODE or a NEXT/PREV
  487. operation, we'll attempt a PARENT-DELETE-UPDATE.
  488.  
  489. PARENT-DELETE-UPDATE should fail when
  490. (a) the key is found but points to a different block (meaning
  491.    the containing span isn't fully reduced);
  492. (b) the block is NAME-locked (this means its safe to leave name-locks
  493.    around, the worst that can happen is they cause block deletes to
  494.    be deferred some);
  495. (c) someone else is doing a PARENT-DELETE-UPDATE on this block
  496.    (this can be guaranteed by having the delete process first
  497.    name-lock the block being deleted;
  498. (d) some block needing to be modified (parent or prev) is locked.
  499.  
  500. 3. We need to fix the code that does the NEXT-KEY operation
  501. to not stop at potentially-dead blocks. CHAIN-FIND need NOT
  502. ignore blank blocks.
  503.  
  504. In practice, we'll want to trigger the update routines at the normal
  505. times as well, ie, try insert-updates after block splits and
  506. delete-updates after a block becomes empty.
  507.  
  508. There are a number of new statistics we should keep; these include:
  509.  
  510. * deferred parent-insert-updates (PUDs)
  511. * insert-updates that succeed
  512. * insert-updates that fail (measures overhead of the method)
  513. * insert-update failure rate
  514.  
  515. * deferred block deletes
  516. * deferred block deletes that succeed
  517. * deferred block deletes that fail (measures overhead of the method)
  518. * delete failure rate
  519.  
  520. * count of dead blocks found (requires extra work)
  521.  
  522. (The number of chain-forwards is also a meaure of the overhead.)
  523.  
  524. [Note: this mechanism aldo supports a simple queuing method: keep a
  525. list of the block numbers at which updates were deferred, and in times
  526. of low usage do FINDs to them, which will update them as a
  527. side-effect...]
  528.  
  529. ------------
  530.  
  531. [Other strategies were considered but were either significantly more
  532. complex or overheady, or introduced unnecessary delays:
  533.  
  534. One hack is to serialize the queue of potponed index INSERT and DELETE
  535. operations by brute force: to do them in exactly the order they
  536. arrived in. Possibly simpler alternative method: sort by key and
  537. timestamp them!
  538.  
  539. I think we also decided that the interpreter could simply devote a
  540. process to each deferred operation, since we want to shift resources
  541. toward accomplishing the deferred ops if too many back up.
  542.  
  543. WANG 90 maintains a queue of deleted operation on the DESTINATION
  544. block.  This has the disadvantage that whenever a block is split or
  545. merged the deferred-operation queues have to be split or merged as
  546. well -- uggh!.]
  547.  
  548.  
  549. III. Buffer, I/O, and Free-List Management
  550.  
  551. 1. When to reclaim buffers - age vs. level vs. dirtyness
  552.  
  553. THIS IS A PERENNIAL NUISANCE -- THERE's a complex system in place which
  554. hasnt really been evaluated or tuned, and theres also a proposal by
  555. Jonathan to simply use the first free (or freeable) buffer one finds.
  556.  
  557. 2. UPDATE-ACCESS:
  558.  
  559. It seems to me there was some case where it wasnt
  560. OK to just do a release to #f and an update from there -- but i cant
  561. think of it offhand...
  562.  
  563. 3. Deferred writes of data blocks
  564.  
  565. Note: this is a different sort of deferral from the "deferred index
  566. updates" discussed earlier. Those deferred operations were correctness-
  567. presetrving; these are not. The idea here is that we can reduce i/o
  568. traffic if we can safely lose a "small" number of updates.
  569.  
  570. 3.1 Description and purpose
  571.  
  572. The general idea here is that we can reduce I/O traffic by
  573. deferring writes of leaf blocks, in the sense that the updates can be
  574. lost without compromising the structure of the database.  This only
  575. works where the data updates in question are not critical to database
  576. or application. Currently, we always defer leaf updates -- both PUTs
  577. and REMoves -- to data trees, where a data tree is any tree not of
  578. type DIRECTORY. (DIRECTORY leaf blocks are written to disk after every
  579. update.) The idea is that the user application should have control over
  580. how often the data blocks are written.
  581.  
  582. Also, deferral of PUTs and DELETES should be separately controllable.
  583. This feature is needed for example by the database itself in
  584. maintaining the free list: we can afford to defer INSERTS of deleted
  585. block, because the worst that could happen is that a free block gets
  586. lost. But DELETES from the free list must update immediately, else a
  587. block could be doubly-allocated.
  588.  
  589. 3.2 Implementation
  590.  
  591. The handle is being extended to contain the following boolean values:
  592.  
  593. (a) SAP: save block after PUTs
  594. (b) SAR: save block after REMOVEs
  595. (c) SAC: force block save after cached block changes
  596. (d) FAC: flush buffer entirely after cached block changes
  597.     (not currently implemented -- future functionality)
  598.  
  599. These bits are used for example as follows:
  600.  
  601. DIRECTORY: SAP=SAR=1;
  602. FREE LIST: SAR=1; SAP=SAC=0;
  603. USER DATA: SAP=SAR=SAC=0;
  604.  
  605. The state of these bits can be changed at any time using (HAN-SET-WCB
  606. han new-bits).  The user can insure that the current block has been
  607. written out by calling WRITE-CACHE-BLK, and can insure that all
  608. modified blocks have been written out by calling WRITE-MODIFIED-BLKS.
  609. At the moment, for safety, directory trees force HAN-SAP and HAN-SAR
  610. when opened or created. Also, OPEN-SEG forces the free list write control
  611. bits to be as shown above, regardless of the block type of the free-list.
  612.  
  613. 4. Caching of last (leaf) block used
  614.  
  615. Works great
  616.  
  617. NOT IMPL YET: We could actually be cleverer and cache the parents of
  618. every node, and even the PREVs, using the same TRY-GET-ENT  protocol!
  619.  
  620. 5. Note.
  621.  
  622. Multiple READ access is not supported yet.
  623. READ-READ conflicts can occur. We seem to recall noting that
  624. this limitation kept us from encountering some other problem,
  625. whose identity is lost for the moment.
  626.  
  627. 6. Free-block management
  628.  
  629. Outline:
  630.  
  631. - free list is implemented as a B tree (its root block is in
  632.   block 2 of the file)
  633. - with cache (free-list-cache, one per segment)
  634. - cache gets filled (to 1/2 full) when it empty and a block is needed;
  635.   it gets flushed (to 1/2 full) when its about to overflow.
  636. - cache can be filled either from the free-list-tree or if that's
  637.   empty, the file is extended.
  638. - cache filling is now done efficiently using scan-delete. This
  639.   also means that only one disk write will be done per
  640.   fill (in most cases).
  641. - cache filling is interlocked so only one process can be filling the
  642.   cache at a time.
  643. - currently cache emptying is inefficient as each write of of a block
  644.   to the tree causes an I/O [deferred writes is intended to fix this]
  645.  
  646. Current problems:
  647.  
  648. Last week (1/8) we concluded that (all efforts to date
  649. notwithstanding) there were still failure cases in free-block
  650. management under concurrent operation, because of problems like:
  651.  
  652. - multiple processes can eat up however many blocks are in the cache,
  653. meaning we still haven't handled the case where a PUT to the free list
  654. causes a block split (necessitating a recursive call to FREE-BLOCK).
  655.  
  656. - Conversely, it is possible for the cache to become overfilled
  657. by a cache filling operation, if in the meantime OTHER processes fill
  658. the cache with deleted blocks.
  659.  
  660. - We need to be sure in general that concurrent fills and/or empties
  661. don't overfill or overempty the cache.
  662.  
  663. On the positive side, we realized that we NEED NOT allow enough
  664. free blocks for a free-list insert to split the whole tree -- any
  665. split except the leaf split can simply be postponed if there isnt a
  666. free block available at that time! (And similarly for any insert-updates
  667. that happen to be triggered by free-list accesses.)
  668.  
  669. Come to think of it, if we happen to run out of blocks during a
  670. free-list insert, its OK to let the insert fail, we just lose one disk
  671. block!! That may just be the answer!!
  672.  
  673. 7. Buffer management routines
  674.  
  675. Two routines have been built for purging buffers.
  676.  
  677. FLUSH-BUFFER(ENT) writes out ENT if it is dirty and unlocked.
  678. It returns TERMINATED if ENT is locked, RETRYERR if the write is
  679. attempted and fails, and SUCCESS otherwise.
  680.  
  681. PURGE-BUFFER(ENT) writes out ENT if it is dirty and then
  682. frees up the buffer. This IGNORES the acceess status of the buffer,
  683. so it sould not be called by users; it always sreturns SUCCESS.
  684.  
  685. Use (DO-SEG-BUFFERS SEG# FUNC) to apply a function to all the buffers
  686. of a given segment; for example, (DO-SEG-BUFFERS SEG# FLUSH-BUFFER)
  687. can be used to guarantee that segment SEG#'s disk file is up to date.
  688. DO-SEG-BUFFERS halts if FUNC returns other than SUCCESS; the result of
  689. FUNC is returned. SUCCESS is returned if all buffers have been
  690. successfully processed.  To process all segments, use SEG #=-1.
  691.  
  692. (CHECK-BUFFER ENT) chekcs that the buffer is written and unlocked,
  693. and repairs those that are not.
  694.  
  695.  
  696. 8. Other issues to document:
  697.  
  698. - bucket-locking method
  699. - treatment of access conflict conditions
  700. - amnesia-ent
  701. - caching criteria (eh?)
  702.  
  703. IV. Error handling
  704.  
  705. The problem: Calls need to return adequate information to distinguish:
  706.   1. restartable operations, for example
  707.     - update-parent on insert
  708.     - update-parent on delete
  709.     - unlink for delete
  710.   2. non-restartable failures (eg, disk i/o error)
  711.   3. null results (eg, key not found)
  712.   4. "real values", eg, the length of a returned string, or
  713.      an  ENT pointer vs. NULL.
  714.  
  715. The solution: use a bounded range of negative values as "failure"
  716. codes, leaving the non-negative return values available as "success"
  717. codes. The canonical success code is 0, but a routine that needs to
  718. return a value (string length, ENT pointer) can do so and have that
  719. value interpreted as a "success" code as well.
  720.  
  721. There are "degrees" of failure,and the negative codes are partitioned
  722. according to increasingly severity, as follows:
  723.  
  724. SUCCESS 0    ; successful execution
  725. NOTPRES -1    ; successful execution, no data present or no change made
  726. TERMINATED -2    ; failure, no damage, caller can retry operation
  727.  
  728. RETRYERR -10    ; failure, no damage, caller can retry operation
  729. ARGERR -15    ; failure, no damage, call was in error
  730.  
  731. NOROOM -20    ; failure, no damage, out of room in file
  732. TYPERR -30    ; failure, file or object was not of correct type
  733.  
  734. IOERR -40    ; i/o error, DB may be damaged
  735. STRANGERR -45    ; internal error, DB may be damaged
  736. UNKERR -90      ; placeholder code
  737. MAXERR -100
  738.  
  739. The first class represent operations that completed without error.
  740. The second class represent operations that failed to complete,
  741. but are guarantedd to leve the DB in a correct state and are
  742. retryable (or easily correctible).
  743. The third class represent operations that failed to complete,
  744. did not damage the database, but are not easily fixable or restartable.
  745. The last class represent error conditions in which the DB was
  746. corrupted, or diuring which DB corruption was detected.
  747.  
  748. The predicate (ERR? code) returns #t if the return code is
  749. within the range NOTPRES-MAXERR; the predicate (REALERR? code)
  750. returns #t if CODE is an actual error, as opposed to a "not there"
  751. or "stop processing" message.
  752.  
  753. V. Misc
  754.  
  755. 1. "Largest keys" (End-of-chain marker strings)
  756.  
  757. Because we've reserved keys with a first byte of FF for split keys,
  758. these keys are unavailable to the user.
  759.  
  760. 2. NULL keys
  761.  
  762. WB supports use of the null string as a key (Sliced Bread treats
  763. the key specially).
  764.  
  765. 3. Searching on minimum and maximum keys
  766.  
  767. Given that the null string may be used as a key, there needs to be a
  768. way to specify a "least" key such that NEXT(least) yields the first
  769. key, even if it is NULL. The special key strings with length s of -2
  770. and -1 are provided to represent the minimum and maximum key values,
  771. respectively.  These keys are only useful with NEXT and PREV; data
  772. cannot be associated with these keys.
  773.  
  774. 4. Need to document TSCAN, TSTATS.
  775.